#include "BufferManager.h"

/**
Create a new instance that manages size frames and operates on the file filename.
*/
BufferManager::BufferManager(const std::string& filename, unsigned size) : bufferSize(size)
{
    this->frames = new BufferFrame*[size];

    SEM_CREATE(this->indexSemaphore);
    SEM_CREATE(this->frameQueueSemaphore);
    SEM_CREATE(this->fileSystemLock);
    SEM_CREATE(this->hitSemaphore);

    this->arrayLocks = new SEMAPHORE[size];

    for (unsigned i = 0; i < size; ++i)
    {
        SEM_CREATE(this->arrayLocks[i]);
    }

    file = fopen(filename.c_str(), "r+");
    if (file == NULL)
    {
        cout << "Something bad happened: error while opening file." << endl;
        exit(1);
    }

    this->bufferHits = 0;
    this->bufferMisses = 0;
}

/**
A method to retrieve frames given a page ID and indicating whether the page will be
held exclusively by this thread or not. The method can fail if no free frame is
available and no used frame can be freed.
*/
BufferFrame& BufferManager::getPage(unsigned pageId, bool exclusive)
{
    int position = this->getIndex(pageId);

    if (position == -1)
    {
        //frame not found
        position = this->swapPage();

        // increment MISSES counter
        this->bufferMisses += 1;

        if (position == -1)
        {
            //no free frame...now we're doomed
            cout << "Something bad happened: Sry dude, no buffer-space left." << endl;
            exit(1);
        }

        this->frames[position] = this->loadPage(pageId);
        BufferFrame& frame = *(this->frames[position]);
        frame.doLock(exclusive);

        SEM_WRITELOCK(this->indexSemaphore);
        this->index[pageId] = position;
        SEM_UNLOCK(this->indexSemaphore);

        SEM_WRITELOCK(this->frameQueueSemaphore);
        this->frameQueue.push(position);
        SEM_UNLOCK(this->frameQueueSemaphore);

        SEM_UNLOCK(this->arrayLocks[position]);

        return frame;
    }

    // increment HIT counter
    SEM_WRITELOCK(this->hitSemaphore);
    this->bufferHits += 1;
    SEM_UNLOCK(this->hitSemaphore);

    SEM_READLOCK(this->arrayLocks[position]);

    BufferFrame& frame = *(this->frames[position]);

    if (frame.getPageId() != pageId)
    {
        cout << "soll " << pageId << " ist " << frame.getPageId() << endl;
        SEM_UNLOCK(this->arrayLocks[position]);
        return this->getPage(pageId, exclusive);
    }

    frame.doLock(exclusive);
    SEM_UNLOCK(this->arrayLocks[position]);

    //frame.setSecondChance(true);

    return frame;
}


BufferFrame* BufferManager::loadPage(unsigned pageId)
{
    char* data = new char[pageSize];
    SEM_WRITELOCK(this->fileSystemLock);

    if (fseek(this->file, pageId * pageSize, SEEK_SET) != 0)
    {
        cout << "Something bad happened: error while seeking file." << endl;
        exit(1);
    }

    if (fread(data, sizeof(char), pageSize, this->file) != pageSize)
    {
        cout << "Something bad happened: error while reading file. pageId: " << pageId << endl;
        exit(1);
    }
    SEM_UNLOCK(this->fileSystemLock);

    return new BufferFrame(pageId, data);
}

/**
Swaps a unused page to disc and returns the index of the freed page.
Return -1 if no frame is free. If x is returned, this->arrayLocks[x] is locked.
*/
int BufferManager::swapPage()
{
    SEM_WRITELOCK(this->frameQueueSemaphore);

    if (this->frameQueue.size() == 0)
    {
        SEM_UNLOCK(this->frameQueueSemaphore);
        return -1; //failure
    }

    for (unsigned i = 0; i < 2 * this->frameQueue.size(); i++)
    {
        unsigned position = this->frameQueue.front();
        this->frameQueue.pop();

        if (SEM_TRYWRITELOCK(this->arrayLocks[position]) != 0)
        {
            //lock failed -> frame is in use
            this->frameQueue.push(position);
        }
        else
        {
            //lock successfull
            BufferFrame& frame = *(this->frames[position]);

            if (frame.tryLock() != 0)
            {
                //lock failed -> frame is in use
                this->frameQueue.push(position);
            }
            else
            {
                //lock successfull
                if (frame.hasSecondChance())
                {
                    frame.setSecondChance(false);
                    this->frameQueue.push(position);
                    frame.releaseLock();
                }
                else
                {
                    SEM_UNLOCK(this->frameQueueSemaphore);
                    if (!this->frames[position]->isDummy())
                    {
                        SEM_WRITELOCK(this->indexSemaphore);
                        this->index[this->frames[position]->getPageId()] = -1;
                        SEM_UNLOCK(this->indexSemaphore);
                    }
                    this->writeToDisk(frame);
                    delete this->frames[position];
                    this->frames[position] = NULL;

                    return position;
                }
            }
            SEM_UNLOCK(this->arrayLocks[position]);
        }
    }
    SEM_UNLOCK(this->frameQueueSemaphore);
    return -1;
}

/**
Writes the data of the given frame back to disk if the frame is marked as dirty.
*/
void BufferManager::writeToDisk(BufferFrame& frame)
{
    if (frame.isDirty())
    {
        SEM_WRITELOCK(this->fileSystemLock);
        if (fseek(this->file, frame.getPageId() * pageSize, SEEK_SET) != 0)
        {
            cout << "Something bad happened: error while seeking file." << endl;
            exit(1);
        }
        if (fwrite(frame.getData(), sizeof(char), pageSize, this->file) != pageSize)
        {
            cout << "Something bad happened: error while writing file." << endl;
            exit(1);
        }

        fflush(this->file);

        SEM_UNLOCK(this->fileSystemLock);
    }
}

/**
Returns the position of the page with the given pageId or -1 if the the Buffer does not contain the page.
*/
int BufferManager::getIndex(unsigned pageId)
{
    SEM_READLOCK(this->indexSemaphore);

    map<unsigned, int>::iterator it = this->index.find(pageId);

    if (it != this->index.end())
    {
        if (it->first == pageId)
        {
            SEM_UNLOCK(this->indexSemaphore);
            return it->second;
        }
    }
    SEM_UNLOCK(this->indexSemaphore);

    return -1;
}

/**
Returns the number of buffer hits (i.e. requested page is buffered)
*/
int BufferManager::getBufferHits()
{
    return this->bufferHits;
}

/**
Returns the number of buffer misses (i.e. requested page has to be loaded from disk)
*/
int BufferManager::getBufferMisses()
{
    return this->bufferMisses;
}


/**
Return a frame to the buffer manager indicating whether it is dirty or not. If dirty, the page
manager must write it back to disk. It does not have to write it back immediately,
but must not write it back before unfixPage is called.
*/
void BufferManager::unfixPage(BufferFrame& frame, bool isDirty)
{
    frame.markDirty(isDirty);
    frame.releaseLock();
}

BufferManager::~BufferManager()
{
    for (unsigned i = 0; i < this->bufferSize; i++)
    {
        if (this->frames[i])
        {
            this->writeToDisk(*(this->frames[i]));
        }
    }

    SEM_DESTROY(this->indexSemaphore);
    SEM_DESTROY(this->frameQueueSemaphore);
    SEM_DESTROY(this->fileSystemLock);
    SEM_DESTROY(this->hitSemaphore);

    for (unsigned i = 0; i < this->bufferSize; i++)
    {
        SEM_DESTROY(this->arrayLocks[i]);
    }
    delete[] this->arrayLocks;
    for (unsigned i = 0; i < this->bufferSize; i++)
    {
        delete this->frames[i];
        this->frames[i] = NULL;
    }
    delete[] this->frames;
    fclose(file);
}
